/*******************************************************************************
 * Copyright (c) 2011 Google, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Google, Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.wb.core.controls;

import com.google.common.collect.Lists;

import org.eclipse.wb.internal.core.model.property.editor.TextControlActionsManager;
import org.eclipse.wb.internal.core.model.property.table.PropertyTable;
import org.eclipse.wb.internal.core.utils.check.Assert;

import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TypedEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TypedListener;

import java.util.ArrayList;

/**
 * Extended ComboBox control for {@link PropertyTable} and combo property editors.
 *
 * @author sablin_aa
 * @coverage core.control
 */
public class CComboBox extends Composite {
  private Text m_text;
  private Button m_button;
  private Canvas m_canvas;
  private Shell m_popup;
  private TableViewer m_table;
  private boolean m_fullDropdownTableWidth = false;
  private boolean m_wasFocused;

  ////////////////////////////////////////////////////////////////////////////
  //
  // Constructor
  //
  ////////////////////////////////////////////////////////////////////////////
  public CComboBox(Composite parent, int style) {
    super(parent, style);
    createContents(this);
    m_wasFocused = isComboFocused();
    // add display hook
    final Listener displayFocusInHook = new Listener() {
      public void handleEvent(Event event) {
        boolean focused = isComboFocused();
        if (m_wasFocused && !focused) {
          // close DropDown on focus out ComboBox
          comboDropDown(false);
        }
        if (event.widget != CComboBox.this) {
          // forward to ComboBox listeners
          if (!m_wasFocused && focused) {
            event.widget = CComboBox.this;
            notifyListeners(SWT.FocusIn, event);
          }
          if (m_wasFocused && !focused) {
            event.widget = CComboBox.this;
            notifyListeners(SWT.FocusOut, event);
          }
        }
        m_wasFocused = focused;
      }
    };
    final Listener displayFocusOutHook = new Listener() {
      public void handleEvent(Event event) {
        m_wasFocused = isComboFocused();
      }
    };
    {
      Display display = getDisplay();
      display.addFilter(SWT.FocusIn, displayFocusInHook);
      display.addFilter(SWT.FocusOut, displayFocusOutHook);
    }
    // combo listeners
    addControlListener(new ControlAdapter() {
      @Override
      public void controlResized(ControlEvent e) {
        resizeInner();
      }
    });
    addDisposeListener(new DisposeListener() {
      public void widgetDisposed(DisposeEvent e) {
        {
          // remove Display hooks
          Display display = getDisplay();
          display.removeFilter(SWT.FocusIn, displayFocusInHook);
          display.removeFilter(SWT.FocusOut, displayFocusOutHook);
        }
        disposeInner();
      }
    });
  }

  ////////////////////////////////////////////////////////////////////////////
  //
  // Contents
  //
  ////////////////////////////////////////////////////////////////////////////
  protected void createContents(Composite parent) {
    createText(parent);
    createButton(parent);
    createImage(parent);
    createPopup(parent);
  }

  /**
   * Create Text widget.
   */
  protected void createText(Composite parent) {
    m_text = new Text(parent, SWT.NONE);
    new TextControlActionsManager(m_text);
    // key press processing
    m_text.addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent e) {
        switch (e.keyCode) {
          case SWT.ESC :
            if (isDroppedDown()) {
              // close dropdown
              comboDropDown(false);
              e.doit = false;
            } else {
              // forward to ComboBox listeners
              notifyListeners(SWT.KeyDown, convert2event(e));
            }
            break;
          case SWT.ARROW_UP :
            if (isDroppedDown()) {
              // prev item in dropdown list
              Table table = m_table.getTable();
              int index = table.getSelectionIndex() - 1;
              table.setSelection(index < 0 ? table.getItemCount() - 1 : index);
              e.doit = false;
            } else {
              // forward to ComboBox listeners
              notifyListeners(SWT.KeyDown, convert2event(e));
            }
            break;
          case SWT.ARROW_DOWN :
            if (isDroppedDown()) {
              // next item in dropdown list
              Table table = m_table.getTable();
              int index = table.getSelectionIndex() + 1;
              table.setSelection(index == table.getItemCount() ? 0 : index);
              e.doit = false;
            } else if ((e.stateMask & SWT.ALT) != 0) {
              // force drop down combo
              comboDropDown(true);
              e.doit = false;
              // return focus to text
              setFocus2Text(false);
            } else {
              // forward to ComboBox listeners
              notifyListeners(SWT.KeyDown, convert2event(e));
            }
            break;
          case '\r' :
            Table table = m_table.getTable();
            if (isDroppedDown() && table.getSelectionIndex() != -1) {
              // forward to Table listeners
              table.notifyListeners(SWT.Selection, convert2event(e));
            } else {
              m_text.selectAll();
              setSelectionText(getEditText());
              // forward to ComboBox listeners
              notifyListeners(SWT.Selection, convert2event(e));
            }
            break;
        }
      }
    });
    // modifications processing
    m_text.addModifyListener(new ModifyListener() {
      public void modifyText(ModifyEvent e) {
        if (isDroppedDown()) {
          m_table.refresh();
        } else {
          // force drop down combo
          if (m_text.isFocusControl()) {
            comboDropDown(true);
            // return focus to text
            setFocus2Text(false);
          }
        }
      }
    });
  }

  /**
   * Create arrow button.
   */
  protected void createButton(Composite parent) {
    m_button = new Button(parent, SWT.ARROW | SWT.DOWN);
    m_button.addSelectionListener(new SelectionAdapter() {
      @Override
      public void widgetSelected(SelectionEvent e) {
        comboDropDown(!isDroppedDown());
        // return focus to text
        setFocus2Text(true);
      }
    });
  }

  /**
   * Create image canvas.
   */
  protected void createImage(Composite parent) {
    m_canvas = new Canvas(parent, SWT.BORDER);
    m_canvas.addPaintListener(new PaintListener() {
      public void paintControl(PaintEvent e) {
        Image selectionImage = getSelectionImage();
        if (selectionImage != null) {
          e.gc.drawImage(selectionImage, 0, 0);
        } else {
          e.gc.fillRectangle(m_canvas.getClientArea());
        }
      }
    });
  }

  /**
   * Create popup shell with table.
   */
  protected void createPopup(Composite parent) {
    m_popup = new Shell(getShell(), SWT.BORDER);
    m_popup.setLayout(new FillLayout());
    createTable(m_popup);
  }

  /**
   * Create table.
   */
  protected void createTable(Composite parent) {
    m_table = new TableViewer(parent, SWT.FULL_SELECTION);
    new TableViewerColumn(m_table, SWT.LEFT);
    m_table.getTable().addSelectionListener(new SelectionAdapter() {
      @Override
      public void widgetSelected(SelectionEvent e) {
        int selectionIndex = m_table.getTable().getSelectionIndex();
        setSelectionIndex(selectionIndex);
        comboDropDown(false);
        // forward to ComboBox listeners
        notifyListeners(SWT.Selection, convert2event(e));
      }
    });
    m_table.setContentProvider(getContentProvider());
    m_table.setLabelProvider(getLabelProvider());
    m_table.addFilter(getFilterProvider());
  }

  /**
   * Placement inner widgets.
   */
  protected void resizeInner() {
    Rectangle clientArea = getClientArea();
    int rightOccupied = 0;
    int leftOccupied = 0;
    {
      // button
      m_button.setBounds(
          clientArea.width - clientArea.height,
          0,
          clientArea.height,
          clientArea.height);
      rightOccupied = clientArea.height;
    }
    {
      Image selectionImage = getSelectionImage();
      if (selectionImage != null) {
        // image
        m_canvas.setSize(clientArea.height, clientArea.height);
        leftOccupied = clientArea.height;
      } else {
        m_canvas.setSize(1, clientArea.height);
        leftOccupied = 1;
      }
    }
    {
      // text
      m_text.setBounds(
          leftOccupied,
          0,
          clientArea.width - rightOccupied - leftOccupied,
          clientArea.height);
    }
  }

  /**
   * Dispose inner widgets.
   */
  protected void disposeInner() {
    if (!m_popup.isDisposed()) {
      m_popup.dispose();
    }
  }

  ////////////////////////////////////////////////////////////////////////////
  //
  // Providers
  //
  ////////////////////////////////////////////////////////////////////////////
  protected IContentProvider getContentProvider() {
    return new IStructuredContentProvider() {
      public Object[] getElements(Object inputElement) {
        return m_items.toArray(new ComboBoxItem[m_items.size()]);
      }

      public void dispose() {
      }

      public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
      }
    };
  }

  protected IBaseLabelProvider getLabelProvider() {
    return new LabelProvider() {
      @Override
      public Image getImage(Object element) {
        ComboBoxItem item = (ComboBoxItem) element;
        return item.m_image;
      }

      @Override
      public String getText(Object element) {
        ComboBoxItem item = (ComboBoxItem) element;
        return item.m_label;
      }
    };
  }

  protected ViewerFilter getFilterProvider() {
    return new ViewerFilter() {
      @Override
      public boolean select(Viewer viewer, Object parentElement, Object element) {
        String lookingString = m_text.getText().toLowerCase();
        if (isDroppedDown() && lookingString.length() > 0) {
          ComboBoxItem item = (ComboBoxItem) element;
          return item.m_label.toLowerCase().indexOf(lookingString) != -1;
        }
        return true;
      }
    };
  }

  ////////////////////////////////////////////////////////////////////////////
  //
  // Items
  //
  ////////////////////////////////////////////////////////////////////////////
  protected static class ComboBoxItem {
    public final String m_label;
    public final Image m_image;

    public ComboBoxItem(String label, Image image) {
      m_label = label;
      m_image = image;
    }
  }

  ArrayList<ComboBoxItem> m_items = Lists.newArrayList();

  /**
   * Add new item.
   */
  public void addItem(String label, Image image) {
    Assert.isTrue(!isDroppedDown());
    m_items.add(new ComboBoxItem(label, image));
  }

  public void addItem(String label) {
    addItem(label, null);
  }

  public void removeAll() {
    m_items.clear();
  }

  public int getItemCount() {
    return m_items.size();
  }

  public String getItemLabel(int index) {
    return m_items.get(index).m_label;
  }

  ////////////////////////////////////////////////////////////////////////////
  //
  // Access
  //
  ////////////////////////////////////////////////////////////////////////////
  public boolean isComboFocused() {
    return isFocusControl()
        || m_text.isFocusControl()
        || m_button.isFocusControl()
        || m_canvas.isFocusControl()
        || m_popup.isFocusControl()
        || m_table.getTable().isFocusControl();
  }

  /**
   * Edit text.
   */
  public String getEditText() {
    return m_text.getText();
  }

  public void setEditText(String text) {
    m_text.setText(text == null ? "" : text);
    m_text.selectAll();
  }

  public void setEditSelection(int start, int end) {
    m_text.setSelection(start, end);
  }

  /**
   * Read only.
   */
  public void setReadOnly(boolean value) {
    m_text.setEditable(!value);
    m_button.setEnabled(!value);
  }

  /**
   * Drop down width.
   */
  public boolean isFullDropdownTableWidth() {
    return m_fullDropdownTableWidth;
  }

  public void setFullDropdownTableWidth(boolean value) {
    Assert.isTrue(!isDroppedDown());
    m_fullDropdownTableWidth = value;
  }

  ////////////////////////////////////////////////////////////////////////////
  //
  // Selection
  //
  ////////////////////////////////////////////////////////////////////////////
  private int m_selectionIndex = -1;

  /**
   * Selection index.
   */
  public int getSelectionIndex() {
    return m_selectionIndex;
  }

  public void setSelectionIndex(int index) {
    m_selectionIndex = index;
    if (isDroppedDown()) {
      m_table.getTable().setSelection(m_selectionIndex);
    }
    setEditText(getSelectionText());
  }

  /**
   * Selection text.
   */
  private String getSelectionText() {
    if (m_selectionIndex != -1 && isDroppedDown()) {
      Object itemData = m_table.getTable().getItem(m_selectionIndex).getData();
      return ((ComboBoxItem) itemData).m_label;
    }
    return null;
  }

  /**
   * Selection image.
   */
  private Image getSelectionImage() {
    return m_selectionIndex != -1 ? m_items.get(m_selectionIndex).m_image : null;
  }

  public void setSelectionText(String label) {
    TableItem[] items = m_table.getTable().getItems();
    for (int i = 0; i < items.length; i++) {
      TableItem item = items[i];
      if (item.getText().equals(label)) {
        setSelectionIndex(i);
        return;
      }
    }
    // no such item
    setSelectionIndex(-1);
    setEditText(label);
  }

  /**
   * Adds the listener to receive events.
   */
  public void addSelectionListener(SelectionListener listener) {
    checkWidget();
    if (listener == null) {
      SWT.error(SWT.ERROR_NULL_ARGUMENT);
    }
    TypedListener typedListener = new TypedListener(listener);
    addListener(SWT.Selection, typedListener);
    addListener(SWT.DefaultSelection, typedListener);
  }

  ////////////////////////////////////////////////////////////////////////////
  //
  // Popup
  //
  ////////////////////////////////////////////////////////////////////////////
  public boolean isDroppedDown() {
    return m_popup.isVisible();
  }

  public void comboDropDown(boolean dropdown) {
    // check, may be we already in this drop state
    if (dropdown == isDroppedDown()) {
      return;
    }
    // close combo
    if (dropdown) {
      // initialize
      m_table.setInput(m_items);
      Table table = m_table.getTable();
      TableColumn column = table.getColumn(0);
      column.pack();
      table.pack();
      m_popup.pack();
      // compute table size
      Rectangle tableBounds = table.getBounds();
      tableBounds.height = Math.min(tableBounds.height, table.getItemHeight() * 15);// max 15 items without scrolling
      table.setBounds(tableBounds);
      // prepare popup point
      Point comboLocation = toDisplay(new Point(0, 0));
      Point comboSize = getSize();
      // compute popup size
      Display display = getDisplay();
      Rectangle clientArea = display.getClientArea();
      int remainingDisplayHeight = clientArea.height - comboLocation.y - comboSize.y - 10;
      int preferredHeight = Math.min(tableBounds.height, remainingDisplayHeight);
      int remainingDisplayWidth = clientArea.width - comboLocation.x - 10;
      int preferredWidth =
          isFullDropdownTableWidth()
              ? Math.min(tableBounds.width, remainingDisplayWidth)
              : comboSize.x;
      Rectangle popupBounds =
          new Rectangle(comboLocation.x,
              comboLocation.y + comboSize.y,
              preferredWidth,
              preferredHeight);
      Rectangle trimBounds =
          m_popup.computeTrim(popupBounds.x, popupBounds.y, popupBounds.width, popupBounds.height);
      m_popup.setBounds(popupBounds.x, popupBounds.y, 2 * popupBounds.width - trimBounds.width, 2
          * popupBounds.height
          - trimBounds.height);
      // adjust column size
      column.setWidth(table.getClientArea().width);
      // show popup
      m_popup.setVisible(true);
      table.setSelection(getSelectionIndex());
    } else {
      // hide popup
      m_popup.setVisible(false);
    }
  }

  protected final void setFocus2Text(final boolean selectAll) {
    getDisplay().asyncExec(new Runnable() {
      final boolean m_selectAll = selectAll;

      public void run() {
        if (!m_text.isDisposed()) {
          m_text.setFocus();
          if (m_selectAll) {
            m_text.selectAll();
          }
        }
      }
    });
  }

  ////////////////////////////////////////////////////////////////////////////
  //
  // Utilities
  //
  ////////////////////////////////////////////////////////////////////////////
  protected static Event convert2event(TypedEvent tEvent) {
    Event event = new Event();
    event.widget = tEvent.widget;
    event.display = tEvent.display;
    event.widget = tEvent.widget;
    event.time = tEvent.time;
    event.data = tEvent.data;
    if (tEvent instanceof KeyEvent) {
      KeyEvent kEvent = (KeyEvent) tEvent;
      event.character = kEvent.character;
      event.keyCode = kEvent.keyCode;
      event.stateMask = kEvent.stateMask;
      event.doit = kEvent.doit;
    }
    if (tEvent instanceof SelectionEvent) {
      SelectionEvent sEvent = (SelectionEvent) tEvent;
      event.item = sEvent.item;
      event.x = sEvent.x;
      event.y = sEvent.y;
      event.width = sEvent.width;
      event.height = sEvent.height;
      event.detail = sEvent.detail;
      event.stateMask = sEvent.stateMask;
      event.text = sEvent.text;
      event.doit = sEvent.doit;
    }
    return event;
  }
}
